﻿/*
 *  Copyright (C) 2010 by Benjamin Nolmans <benjamin.nolmans@gmail.com>
 *  and Barbara Post <postb99@gmail.com>
 *  Copyright (C) 2010 by Barbara Post postb99@gmail.com>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */
using System;
using System.Collections.Generic;
using NLog;
using NLog.Config;
using NLog.Targets;
using OsamesFrameworkCore.Configuration;
using OsamesFrameworkCore.Plugins;
using OsamesFrameworkCore.Plugins.Tools;
using OsamesFrameworkCore.Tools;

namespace OsamesFrameworkCore.Logging
{
    public static class LoggerService
    {
        // NLog logging targets as defined in web.nlog.
        private const string CST_STR_PLUGIN_TRACE_LOG_TARGET = "logfile_trace_plugin";
        private const string CST_STR_PLUGIN_DEBUG_LOG_TARGET = "logfile_debug_plugin";
        private const string CST_STR_PLUGIN_ERROR_LOG_TARGET = "logfile_error_plugin";
        private const string CST_STR_PLUGIN_FATAL_LOG_TARGET = "logfile_fatal_plugin";
        private const string CST_STR_PLUGIN_WARN_LOG_TARGET = "logfile_warn_plugin";
        private const string CST_STR_PLUGIN_INFO_LOG_TARGET = "logfile_info_plugin";

        /// <summary>
        /// To keep a list of logger names we have dynamically generated and added to nlog config.
        /// </summary>
        private static readonly List<string> _lstDynamicallyCreatedLoggersNames = new List<string>();

        /// <summary>
        /// Determined whether a log level is active. Avoids to do string formatting uselessly.
        /// </summary>
        /// <param name="loggableClass_">Loggable class which wants to use logging</param>
        /// <param name="eLogLevel_">Log level we want to check it's active or not</param>
        /// <returns>true/false</returns>
        public static bool IsEnabled(ILoggable loggableClass_, LogLevel eLogLevel_)
        {
            if (ConfigurationPreventsLogging(eLogLevel_))
                return false;

            if (loggableClass_ is IPlugin)
            {
                LogLevel ePluginConfigLogLevel = GetPluginLogLevel((IPlugin)loggableClass_);
                return ((int)ePluginConfigLogLevel) >= ((int)eLogLevel_);
            }
            // ILoggable but not a plugin
            Logger logger = GetNlogLogger(loggableClass_.GetType().FullName);
            switch ((int)eLogLevel_)
            {
                case (int)LogLevel.Trace:
                    return logger.IsTraceEnabled;
                case (int)LogLevel.Debug:
                    return logger.IsDebugEnabled;
                case (int)LogLevel.Info:
                    return logger.IsInfoEnabled;
                case (int)LogLevel.Warn:
                    return logger.IsWarnEnabled;
                case (int)LogLevel.Error:
                    return logger.IsFatalEnabled;
                case (int)LogLevel.Fatal:
                    return logger.IsFatalEnabled;
            }
            return false;
        }

        /// <summary>
        /// Determined whether a log level is active. Avoids to do string formatting uselessly.
        /// </summary>
        /// <param name="tMaybeILoggable_">Type, may be a IPlugin or ILoggable</param>
        /// <param name="eLogLevel_">Log level we want to check it's active or not</param>
        /// <returns>true/false</returns>
        public static bool IsEnabled(Type tMaybeILoggable_, LogLevel eLogLevel_)
        {
            if (ConfigurationPreventsLogging(eLogLevel_))
                return false;

            if (tMaybeILoggable_ is IPlugin)
            {
                LogLevel ePluginConfigLogLevel = GetPluginLogLevel((IPlugin)tMaybeILoggable_);
                return ((int)ePluginConfigLogLevel) >= ((int)eLogLevel_);
            }
            // Not a plugin
            Logger logger = GetNlogLogger(tMaybeILoggable_.FullName);
            switch ((int)eLogLevel_)
            {
                case (int)LogLevel.Trace:
                    return logger.IsTraceEnabled;
                case (int)LogLevel.Debug:
                    return logger.IsDebugEnabled;
                case (int)LogLevel.Info:
                    return logger.IsInfoEnabled;
                case (int)LogLevel.Warn:
                    return logger.IsWarnEnabled;
                case (int)LogLevel.Error:
                    return logger.IsFatalEnabled;
                case (int)LogLevel.Fatal:
                    return logger.IsFatalEnabled;
            }
            return false;
        }

        /// <summary>
        /// Log to console. Useful during development for easy debugging.
        /// </summary>
        /// <param name="strDebug_">debug string</param>
        public static void LogToOutput(string strDebug_)
        {
            System.Diagnostics.Debug.Print(strDebug_);
        }

        /// <summary>
        /// Logging passing a reference to an ILoggable class.
        /// </summary>
        /// <param name="loggableClass_">Class of type ILoggable, may also be a IPlugin</param>
        /// <param name="eLogLevel_">Log level</param>
        /// <param name="strMessage_">Message to log</param>
        /// <param name="strMessageDetails_">Optional array of message details. In this case, strMessage_ parameter contains placeholders, each matching a value in strMessageDetails_</param>
        public static void Log(ILoggable loggableClass_, LogLevel eLogLevel_, string strMessage_, object[] strMessageDetails_ = null)
        {
            if (ConfigurationPreventsLogging(eLogLevel_))
                return;

            // Nlog logger
            Logger logger;

            if (loggableClass_ is IPlugin)
            {
                // IPlugin : find/create dynamic logger
                LogLevel ePluginConfigLogLevel = GetPluginLogLevel((IPlugin)loggableClass_);
                logger = GetNlogLogger((IPlugin)loggableClass_, ePluginConfigLogLevel);
                LogToOutput("Plugin '" + PluginsTools.GetPluginUniqueId((IPlugin)loggableClass_) + "' configuration log level is '" + ePluginConfigLogLevel + "'");
            }
            else
            {
                // ILoggable but not a plugin
                logger = GetNlogLogger(loggableClass_);
            }
            ReallyLog(logger, eLogLevel_, strMessage_, strMessageDetails_);
        }

        /// <summary>
        /// Logging method to use in static functions.
        /// </summary>
        /// <param name="tMaybeILoggable_">Type, may be a ILoggable/IPlugin</param>
        /// <param name="eLogLevel_">Log level</param>
        /// <param name="strMessage_">Message to log</param>
        /// <param name="strMessageDetails_">Optional array of message details. In this case, strMessage_ parameter contains placeholders, each matching a value in strMessageDetails_</param>
        public static void Log(Type tMaybeILoggable_, LogLevel eLogLevel_, string strMessage_, object[] strMessageDetails_ = null)
        {
            if (ConfigurationPreventsLogging(eLogLevel_))
                return;
            
            if (tMaybeILoggable_ is ILoggable)
                Log((ILoggable)tMaybeILoggable_, eLogLevel_, strMessage_, strMessageDetails_);
            else
            {
                Logger logger = GetNlogLogger(tMaybeILoggable_);
                ReallyLog(logger, eLogLevel_, strMessage_, strMessageDetails_);
            }
        }

         /// <summary>
        /// Logging passing a reference to an ILoggable class.
        /// </summary>
        /// <param name="loggableClass_">Class of type ILoggable, may also be a IPlugin</param>
        /// <param name="eLogLevel_">Log level</param>
        /// <param name="strMessage_">Message to log</param>
        /// <param name="ex_">Exception whose message and stack trace will be logged</param>
        public static void Log(ILoggable loggableClass_, LogLevel eLogLevel_, string strMessage_, Exception ex_)
         {
            Log(loggableClass_, eLogLevel_, "{0} Exception : {1} {2}", new object[] {strMessage_, ex_.Message, ex_.StackTrace});
         }

        /// <summary>
        /// Logging method to use in static functions.
        /// </summary>
        /// <param name="tMaybeILoggable_">Type, may be a ILoggable/IPlugin</param>
        /// <param name="eLogLevel_">Log level</param>
        /// <param name="strMessage_">Message to log</param>
        /// <param name="ex_">Exception whose message and stack trace will be logged</param>
        public static void Log(Type tMaybeILoggable_, LogLevel eLogLevel_, string strMessage_, Exception ex_)
        {
            Log(tMaybeILoggable_, eLogLevel_, "{0} Exception : {1} {2}", new object[] { strMessage_, ex_.Message, ex_.StackTrace });
        }

        /// <summary>
        /// Instantiates a Nlog logger which name is parameter class type full name.
        /// </summary>
        /// <param name="loggableClass_">Class of type ILoggable</param>
        /// <returns>Nlog logger according to logging rules</returns>
        private static Logger GetNlogLogger(ILoggable loggableClass_)
        {
            return LogManager.GetLogger(loggableClass_.GetType().FullName);
        }

        /// <summary>
        /// Instantiates a Nlog logger which name is type full name.
        /// </summary>
        /// <param name="tType_">Class type</param>
        /// <returns>Nlog logger according to logging rules</returns>
        private static Logger GetNlogLogger(Type tType_)
        {
            return LogManager.GetLogger(tType_.FullName);
        }

        /// <summary>
        /// Instantiates a Nlog logger which name is class type full name.
        /// </summary>
        /// <param name="strClassName_">Class type full name</param>
        /// <returns>Nlog logger according to logging rules</returns>
        private static Logger GetNlogLogger(string strClassName_)
        {
            return LogManager.GetLogger(strClassName_);
        }

        /// <summary>
        /// Dynamic Nlog configuration for plugins.
        /// Instantiates as many Nlog loggers as required by threshold log level.
        /// Target names are referenced by CST_STR_PLUGIN_*_LOG_TARGET
        /// </summary>
        /// <param name="iPlugin_">plugin</param>
        /// <param name="eThresholdLogLevel_">Threshold log level (plugin will not use lower log level)</param>
        /// <returns>Nlog logger according to (dynamically added) logging rules</returns>
        private static Logger GetNlogLogger(IPlugin iPlugin_, LogLevel eThresholdLogLevel_)
        {
            string strClassName = iPlugin_.GetType().FullName;
            // Did we already create loggers for this class ? if so, use it from Nlog loggers collection.
            if (_lstDynamicallyCreatedLoggersNames.Contains(strClassName))
                return LogManager.GetLogger(strClassName);

            // We must create a logger for each active log level. So we make a list
            // of targets we will use for our loggers definitions.
            // It means that if a plugin has error level set, it's useless to create debug and trace loggers.
            List<string> lstNlogTargetNames = new List<string>();
            List<NLog.LogLevel> lstNlogLogLevels = new List<NLog.LogLevel>();
            int iThreshold = (int)eThresholdLogLevel_;

            if (iThreshold <= (int)LogLevel.Trace)
            {
                lstNlogTargetNames.Add(CST_STR_PLUGIN_TRACE_LOG_TARGET);
                lstNlogLogLevels.Add(NLog.LogLevel.Trace);
            }
            if (iThreshold <= (int)LogLevel.Debug)
            {
                lstNlogTargetNames.Add(CST_STR_PLUGIN_DEBUG_LOG_TARGET);
                lstNlogLogLevels.Add(NLog.LogLevel.Debug);
            }
            if (iThreshold <= (int)LogLevel.Info)
            {
                lstNlogTargetNames.Add(CST_STR_PLUGIN_INFO_LOG_TARGET);
                lstNlogLogLevels.Add(NLog.LogLevel.Info);
            }
            if (iThreshold <= (int)LogLevel.Warn)
            {
                lstNlogTargetNames.Add(CST_STR_PLUGIN_WARN_LOG_TARGET);
                lstNlogLogLevels.Add(NLog.LogLevel.Warn);
            }
            // Always add error and fatal loggers
            lstNlogTargetNames.Add(CST_STR_PLUGIN_ERROR_LOG_TARGET);
            lstNlogLogLevels.Add(NLog.LogLevel.Error);
            lstNlogTargetNames.Add(CST_STR_PLUGIN_FATAL_LOG_TARGET);
            lstNlogLogLevels.Add(NLog.LogLevel.Fatal);

            int iCount = lstNlogTargetNames.Count;
            for (int i = 0; i < iCount; i++)
            {
                Target tNlogTarget = LogManager.Configuration.FindTargetByName(lstNlogTargetNames[i]);
                LoggingRule lrNlogLoggingRule = new LoggingRule(strClassName, lstNlogLogLevels[i], tNlogTarget);
                LogManager.Configuration.LoggingRules.Add(lrNlogLoggingRule);

                //LogToOutput(string.Format("Dynamically added nlog logger for class '{0}' with level {1} and target '{2}'",
                //    strClassName_, lstNlogLogLevels[i], lstNlogTargetNames[i]));
            }
            // Add to caching list so as not to recreate later
            _lstDynamicallyCreatedLoggersNames.Add(strClassName);

            Logger logger = LogManager.GetLogger(strClassName);
            LogToOutput(string.Format("For class '{0}', logger is enabled for : {1} {2} {3} {4} {5} {6}",
                strClassName, logger.IsTraceEnabled ? "TRACE" : "", logger.IsDebugEnabled ? "DEBUG" : "",
                logger.IsInfoEnabled ? "INFO" : "", logger.IsWarnEnabled ? "WARN" : "",
                logger.IsErrorEnabled ? "ERROR" : "", logger.IsFatalEnabled ? "FATAL" : ""));
            return logger;
        }

        /// <summary>
        /// Maps Osames' log levels and NLog's ones. Calls NLog logging.
        /// </summary>
        /// <param name="logger_">NLog logger we previously got</param>
        /// <param name="eLogLevel_">Osames' log level</param>
        /// <param name="strMessage_">Message to log</param>
        /// <param name="strMessageDetails_">Optional array of message details. In this case, strMessage_ parameter contains placeholders, each matching a value in strMessageDetails_</param>
        private static void ReallyLog(Logger logger_, LogLevel eLogLevel_, string strMessage_, object[] strMessageDetails_)
        {
            switch ((int)eLogLevel_)
            {
                case (int)LogLevel.Trace:
                    if (logger_.IsTraceEnabled)
                    {
                        if (strMessageDetails_ != null)
                            strMessage_ = string.Format(strMessage_, strMessageDetails_);
                        logger_.Trace(strMessage_);
                    }
                    break;
                case (int)LogLevel.Debug:
                    if (logger_.IsDebugEnabled)
                    {
                        if (strMessageDetails_ != null)
                            strMessage_ = string.Format(strMessage_, strMessageDetails_);
                        logger_.Debug(strMessage_);
                    }
                    break;
                case (int)LogLevel.Fatal:
                    if (logger_.IsFatalEnabled)
                    {
                        if (strMessageDetails_ != null)
                            strMessage_ = string.Format(strMessage_, strMessageDetails_);
                        logger_.Fatal(strMessage_);
                    }
                    break;
                case (int)LogLevel.Error:
                    if (logger_.IsErrorEnabled)
                    {
                        if (strMessageDetails_ != null)
                            strMessage_ = string.Format(strMessage_, strMessageDetails_);
                        logger_.Error(strMessage_);
                    }
                    break;
                case (int)LogLevel.Warn:
                    if (logger_.IsWarnEnabled)
                    {
                        if (strMessageDetails_ != null)
                            strMessage_ = string.Format(strMessage_, strMessageDetails_);
                        logger_.Warn(strMessage_);
                    }
                    break;
                case (int)LogLevel.Info:
                    if (logger_.IsInfoEnabled)
                    {
                        if (strMessageDetails_ != null)
                            strMessage_ = string.Format(strMessage_, strMessageDetails_);
                        logger_.Info(strMessage_);
                    }
                    break;
            }
        }

        /// <summary>
        /// Determines whether configuration disables logging in general or below a certain log level.
        /// </summary>
        /// <returns>true when logging shouldn't be done</returns>
        private static bool ConfigurationPreventsLogging(LogLevel eLogLevel_)
        {
            switch (CoreConfiguration.Instance.LogOverridingState)
            {
                case LogOverridingState.NoLog:
                    // no log at all
                    return true;
                case LogOverridingState.No:
                    // normal case
                    return false;
                default:
                    // There is a log level override.
                    LogLevel eOverridingLogLevel = CoreConfiguration.Instance.GlobalOverridingLogLevel;
                    // if param log level is trace (value : 1) and overriding log level is debug (value : 2), 
                    // don't log (return true).
                    return (int)eLogLevel_ < (int)eOverridingLogLevel;
            }
        }

        /// <summary>
        /// Get plugin log level from its configuration.
        /// </summary>
        /// <param name="iPlugin_"></param>
        /// <returns></returns>
        private static LogLevel GetPluginLogLevel(IPlugin iPlugin_)
        {
            string strLogLevel = ConfigurationTools.FindValue(
                   (iPlugin_).XmlDictPluginConfiguration, "sectionBase", "logLevel");
            return (LogLevel)Enum.Parse(typeof(LogLevel), strLogLevel);

        }
    }
}
